/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.billiard;

import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;

import com.sun.opengl.util.j2d.TextRenderer;

import de.akob.glowifier.Glowifier;
import de.akob.mathematics.MathVector;
import de.akob.misc.JOGLWindowWithMouseNavigation;
import de.akob.misc.OpenGLDrawableGroup;
import de.akob.misc.TranslatedOpenGLDrawable;
import de.akob.modeling.GLModel;
import de.akob.modeling.WavefrontObjLoader;
import de.akob.particles.Particle;
import de.akob.particles.SmokeParticleEngine;
import de.akob.shadow.Shadower;

public class AKindOfBilliard extends JOGLWindowWithMouseNavigation implements KeyListener {
	/**
	 * Configuration menu for the billiard simulation.
	 * 
	 * @todo This should become a more general class. Furthermore, the code is simply ugly :-/.
	 */
	private class ConfigurationMenu {
		private final static int TEXT_FONT_SIZE   = 30;
		private final static int TEXT_BORDER_SIZE = 25;
		private TextRenderer     textRenderer     = null;
		
		/**
		 * Standard constructor.
		 */
		public ConfigurationMenu() {
			textRenderer = new TextRenderer(new Font("SansSerif", Font.BOLD, TEXT_FONT_SIZE));
		}
		
		/**
		 * Draw the configuration menu.
		 */
		public void draw(GLAutoDrawable drawable) {
			GL gl = drawable.getGL();
			
			// save several OpenGL state variables
			gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_CURRENT_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_POLYGON_BIT
			        | GL.GL_LIGHTING_BIT | GL.GL_TEXTURE_BIT);
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glPushMatrix();
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glPushMatrix();
			
			// configure matrices
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glLoadIdentity();
			gl.glOrtho(0., 1., 0., 1., -1., 1.);
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			
			// configuration of OpenGL state
			gl.glDisable(GL.GL_DEPTH_TEST);
			gl.glDisable(GL.GL_CULL_FACE);
			gl.glDisable(GL.GL_LIGHTING);
			gl.glDisable(GL.GL_TEXTURE_2D);
			gl.glEnable(GL.GL_BLEND);
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
			
			// draw overlay
			int numEntries = 17;
			double height = (numEntries * 1.5 * TEXT_FONT_SIZE + 2 * TEXT_BORDER_SIZE) / drawable.getHeight();
			gl.glColor4d(0.75, 0.75, 0.75, 0.50);
			gl.glBegin(GL.GL_QUADS);
			gl.glVertex2d(0.1, 0.9 - height);
			gl.glVertex2d(0.9, 0.9 - height);
			gl.glVertex2d(0.9, 0.9);
			gl.glVertex2d(0.1, 0.9);
			gl.glEnd();
			
			// render menu text
			drawEntry(drawable, "Activate @Gravity", billiardTable.getGravityEnabled(), 0);
			drawEntry(drawable, "Auto @camera", autoCamera, 1);
			drawEntry(drawable, "Auto ligh@t", autoLight, 2);
			drawEntry(drawable, "Draw cue @indicator", drawCueIndicator, 3);
			drawEntry(drawable, "Draw @light position", currentLightIndex, 4);
			drawEntry(drawable, "Draw light s@ource", drawLightSource, 5);
			drawEntry(drawable, "Draw shadow @volumes", drawShadowVolumes, 6);
			drawEntry(drawable, "Draw smoke (@N)", drawSmoke, 7);
			drawEntry(drawable, "Enforce cue stroke (@A)", 8);
			drawEntry(drawable, "Glow: billiard table (@1)", drawBilliardTable_glowified, 9);
			drawEntry(drawable, "Glow: cue indicator (@2)", drawCueIndicator_glowified, 10);
			drawEntry(drawable, "Lessen cue stroke (@Y)", 11);
			drawEntry(drawable, "@Pause (if gravity disabled)", billiardTablePaused, 12);
			drawEntry(drawable, "Reset cue @ball", 13);
			drawEntry(drawable, "@Shadow/Lighting", drawSceneShadowed, 14);
			drawEntry(drawable, "Slow @machine", slowMachine, 15);
			drawEntry(drawable, "Show @help", 16);
			
			// restore saved OpenGL state
			gl.glPopMatrix();
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glPopMatrix();
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glPopAttrib();
		}
		
		/**
		 * Draws the option with the given state at the given row position (starts at 0). Note that the option name has
		 * to contain an '@'-character. The first such character will not be printed, but instead the following
		 * character will be printed in a different color to indicate a keyboard shortcut. Place the '@'-character at
		 * the end of the string if no shortcut indicator is desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, boolean optState, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
			drawEntryName(drawable, optName, rowNumber);
			drawEntryValue(drawable, optState, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * Draws an entry without any option value. Note that the option name has to contain an '@'-character. The first
		 * such character will not be printed, but instead the following character will be printed in a different color
		 * to indicate a keyboard shortcut. Place the '@'-character at the end of the string if no shortcut indicator is
		 * desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
			drawEntryName(drawable, optName, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * Draws the option with the given state at the given row position (starts at 0). Note that the option name has
		 * to contain an '@'-character. The first such character will not be printed, but instead the following
		 * character will be printed in a different color to indicate a keyboard shortcut. Place the '@'-character at
		 * the end of the string if no shortcut indicator is desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, int optValue, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
			drawEntryName(drawable, optName, rowNumber);
			drawEntryValue(drawable, optValue, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * Draws the name of an option entry. Call this method between TextRenderer.beginRendering() and
		 * TextRenderer.endRendering().
		 */
		private void drawEntryName(GLAutoDrawable drawable, String optName, int rowNumber) {
			int width = drawable.getWidth();
			int height = drawable.getHeight();
			int top = (int) Math.round(height * 0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int left = (int) Math.round(width * 0.1) + TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option name
			String subOptName;
			Rectangle2D optNameBounds;
			int shortcutSep = optName.indexOf('@');
			textColor_r = 1.f;
			textColor_g = 1.f;
			textColor_b = 0.f;
			textColor_a = 0.7f;
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			int textPos_x = left;
			int textPos_y = (int) Math.round(top - 1.5 * rowNumber * TEXT_FONT_SIZE);
			subOptName = optName.substring(0, shortcutSep);
			textRenderer.draw(subOptName, textPos_x, textPos_y);
			if (shortcutSep < optName.length() - 1) {
				optNameBounds = textRenderer.getBounds(subOptName);
				textPos_x += optNameBounds.getWidth();
				textColor_r = 0.f;
				textColor_g = 0.f;
				textColor_b = 1.f;
				textColor_a = 0.7f;
				textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
				subOptName = optName.substring(shortcutSep + 1, shortcutSep + 2);
				textRenderer.draw(subOptName, textPos_x, textPos_y);
				if (shortcutSep < optName.length() - 2) {
					textPos_x += textRenderer.getBounds(optName.substring(shortcutSep + 1, shortcutSep + 2)).getWidth();
					textColor_r = 1.f;
					textColor_g = 1.f;
					textColor_b = 0.f;
					textColor_a = 0.7f;
					textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
					subOptName = optName.substring(shortcutSep + 2);
					textRenderer.draw(subOptName, textPos_x, textPos_y);
				}
			}
		}
		
		/**
		 * Draws the value of a boolean option entry. Call this method between TextRenderer.beginRendering() and
		 * TextRenderer.endRendering().
		 */
		private void drawEntryValue(GLAutoDrawable drawable, boolean optState, int rowNumber) {
			int width = drawable.getWidth();
			int height = drawable.getHeight();
			int top = (int) Math.round(height * 0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int right = (int) Math.round(width * 0.9) - TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option value
			String optionStatus;
			if (optState == true) {
				optionStatus = "enabled";
				textColor_r = 0.f;
				textColor_g = 1.f;
				textColor_b = 0.f;
				textColor_a = 0.7f;
			} else {
				optionStatus = "disabled";
				textColor_r = 1.f;
				textColor_g = 0.f;
				textColor_b = 0.f;
				textColor_a = 0.35f;
			}
			Rectangle2D optStateEnabledBounds, optStateDisabledBounds;
			optStateEnabledBounds = textRenderer.getBounds("enabled");
			optStateDisabledBounds = textRenderer.getBounds("disabled");
			double statusText_length = Math.max(optStateEnabledBounds.getWidth(), optStateDisabledBounds.getWidth());
			int textPos_x = (int) (right - statusText_length);
			int textPos_y = (int) Math.round(top - 1.5 * rowNumber * TEXT_FONT_SIZE);
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			textRenderer.draw(optionStatus, textPos_x, textPos_y);
		}
		
		/**
		 * Draws the value of an integer option entry. Call this method between TextRenderer.beginRendering() and
		 * TextRenderer.endRendering().
		 */
		private void drawEntryValue(GLAutoDrawable drawable, int optValue, int rowNumber) {
			int width = drawable.getWidth();
			int height = drawable.getHeight();
			int top = (int) Math.round(height * 0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int right = (int) Math.round(width * 0.9) - TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option value
			textColor_r = 0.f;
			textColor_g = 0.f;
			textColor_b = 0.f;
			textColor_a = 1.0f;
			String renderText = String.valueOf(optValue);
			Rectangle2D optValueBounds = textRenderer.getBounds(renderText);
			double statusText_length = optValueBounds.getWidth();
			int textPos_x = (int) (right - statusText_length);
			int textPos_y = (int) Math.round(top - 1.5 * rowNumber * TEXT_FONT_SIZE);
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			textRenderer.draw(renderText, textPos_x, textPos_y);
		}
	}
	
	// serial version uid
	private static final long    serialVersionUID     = -2538915921064692336L;
	
	// some constants used for auto camera fly mode
	public static final double   AUTOCAMERA_FREQUENCY = 0.1;
	public static final double   AUTOCAMERA_XRADIUS   = 6.0;
	public static final double   AUTOCAMERA_YRADIUS   = 0.5;
	public static final double   AUTOCAMERA_ZRADIUS   = 3.0;
	public static final double   AUTOCAMERA_XOFFSET   = 0.0;
	public static final double   AUTOCAMERA_YOFFSET   = 1.5;
	public static final double   AUTOCAMERA_ZOFFSET   = 0.0;
	
	// some constants used for auto light mode
	public static final double   AUTOLIGHT_FREQUENCY  = 0.05;
	public static final double   AUTOLIGHT_XRADIUS    = 3.00;
	public static final double   AUTOLIGHT_YRADIUS    = 0.50;
	public static final double   AUTOLIGHT_ZRADIUS    = 2.50;
	public static final double   AUTOLIGHT_XOFFSET    = -0.50;
	public static final double   AUTOLIGHT_YOFFSET    = 1.00;
	public static final double   AUTOLIGHT_ZOFFSET    = 0.00;
	
	// positional information for smoke engine/cigarette model
	public static final double[] smokePosition        = { Billiard.DBU_POOLTABLE_8FEET_LENGTH / 2. + 0.13,
	        Billiard.DBU_POOLTABLE_8FEET_HEIGHT + 0.062, -0.15 };
	
	// filenames of miscellaneous models
	public static final String   MODEL_FLOOR          = "models/floor.obj";
	public static final String   MODEL_LIGHTSOURCE    = "models/lightSource.obj";
	public static final String   MODEL_ASHTRAY        = "models/ashtray.obj";
	
	/**
	 * Starts the billiard simulation.
	 */
	public static void main(String[] args) {
		AKindOfBilliard billiardGame = new AKindOfBilliard();
		billiardGame.setVisible(true);
	}
	
	// models for miscellaneous objects
	private TranslatedOpenGLDrawable model_ashtray               = null;
	private GLModel                  model_light                 = null;
	
	// drawable group that holds the scene
	private OpenGLDrawableGroup      scene;
	
	// variables for auto camera fly mode
	private MathVector               cameraPos_backup;
	private double                   rotx_camera_backup;
	private double                   roty_camera_backup;
	private double                   rotz_camera_backup;
	
	// shadower for scene
	private Shadower                 scene_shadowed              = null;
	
	// glowifier for glowing objects
	private Glowifier                billiardTable_glowified     = null;
	private Glowifier                cueIndicator_glowified      = null;
	private Glowifier                light_glowified             = null;
	
	// billiard table
	private DBUPoolTable8Feet        billiardTable               = new DBUPoolTable8Feet();
	
	// configuration menu
	ConfigurationMenu                cfgMenu                     = new ConfigurationMenu();
	
	// some configuration variables
	private boolean                  showConfigurationMenu       = false;
	private boolean                  drawCueIndicator            = false;
	private boolean                  drawCueIndicator_glowified  = false;
	private boolean                  drawBilliardTable_glowified = false;
	private boolean                  drawSceneShadowed           = false;
	private boolean                  drawLightSource             = false;
	private boolean                  drawShadowVolumes           = false;
	private boolean                  billiardTablePaused         = false;
	private boolean                  slowMachine                 = false;
	private boolean                  drawSmoke                   = false;
	private boolean                  autoCamera                  = false;
	private boolean                  autoLight                   = false;
	private int                      currentLightIndex           = 0;
	
	// particle engine for smoke
	private SmokeParticleEngine      smokeEngine                 = new SmokeParticleEngine(400, 2.25, 0.10,
	                                                                     smokePosition[0], smokePosition[1],
	                                                                     smokePosition[2], 2., 2., 0.5, new int[] {
	        Particle.SINUS_CURVE, Particle.FIX_GRAVITY, Particle.SINUS_CURVE });
	
	// light0: directional, white, no ambient, specular
	private float[]                  light0_ambient              = { 0.0f, 0.0f, 0.0f, 0.0f };
	private float[]                  light0_diffuse              = { 0.8f, 0.8f, 0.8f, 1.0f };
	private float[]                  light0_specular             = { 0.8f, 0.8f, 0.8f, 1.0f };
	private float                    light0_spotExponent         = 0.f;
	private float                    light0_spotCutoff           = 180.f;
	private float[][]                light0_position             = { { 0.00f, 2.80f, 0.00f, 1.00f },
	        { 0.75f, 1.00f, -0.60f, 1.00f }                     };
	private float[][]                light0_spotDirection        = { { 0.00f, -1.00f, 0.00f },
	        { -0.25f, -0.25f, 1.00f }                           };
	private float[]                  lastLightPosition           = new float[4];
	
	/**
	 * Standard constructor.
	 */
	public AKindOfBilliard() {
		// configure window
		setTitle("A Kind of Billiard");
		setSize(1000, 1000);
	}
	
	/**
	 * Sets the camera position automatically as an ellipsoid function of the current time.
	 */
	private void applyAutoCameraTransformation(GL gl) {
		// get current time in seconds
		double time = System.currentTimeMillis() / 1000.;
		
		// set current position of camera on ellipsoid around table
		double cameraX = AUTOCAMERA_XRADIUS * Math.cos(2 * Math.PI * time * AUTOCAMERA_FREQUENCY) + AUTOCAMERA_XOFFSET;
		double cameraY = AUTOCAMERA_YRADIUS * Math.sin(2 * Math.PI * time * AUTOCAMERA_FREQUENCY) + AUTOCAMERA_YOFFSET;
		double cameraZ = AUTOCAMERA_ZRADIUS * Math.sin(2 * Math.PI * time * AUTOCAMERA_FREQUENCY) + AUTOCAMERA_ZOFFSET;
		
		// raise camera on the height of the table
		cameraY += billiardTable.getHeight();
		
		// let camera look to the table center
		glu.gluLookAt(cameraX, cameraY, cameraZ, 0, billiardTable.getHeight(), 0, 0, 1, 0);
	}
	
	/**
	 * Sets the light position automatically as an ellipsoid function of the current time. Additionally, this method
	 * will draw a small ball indicating the position of the light.
	 */
	private void applyAutoLightTransformation(GL gl) {
		// get current time in seconds
		double time = System.currentTimeMillis() / 1000.;
		
		// set current light position on ellipsoid around table
		float[] lightPos = new float[3];
		lightPos[0] = (float) (AUTOLIGHT_XRADIUS * Math.cos(2 * Math.PI * time * AUTOLIGHT_FREQUENCY) + AUTOLIGHT_XOFFSET);
		lightPos[1] = (float) (AUTOLIGHT_YRADIUS * Math.sin(2 * Math.PI * time * AUTOLIGHT_FREQUENCY) + AUTOLIGHT_YOFFSET);
		lightPos[2] = (float) (AUTOLIGHT_ZRADIUS * Math.sin(2 * Math.PI * time * AUTOLIGHT_FREQUENCY) + AUTOLIGHT_ZOFFSET);
		
		// raise light on the height of the table
		lightPos[1] += billiardTable.getHeight();
		
		// apply position/direction
		float[] lightDirection = new float[3];
		lightDirection[0] = -lightPos[0];
		lightDirection[1] = -lightPos[1];
		lightDirection[2] = -lightPos[2];
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, lightPos, 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPOT_DIRECTION, lightDirection, 0);
		lastLightPosition = lightPos.clone();
	}
	
	/**
	 * Main display method of the program.
	 */
	@Override
	public void display(GLAutoDrawable drawable) {
		GL gl = drawable.getGL();
		
//		// check whether we are in slow machine mode
//		if (slowMachine == true)
//			billiardTable.pause_time();
		
		// clear screen and save some OpenGL state variables
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glPushMatrix();
		
		// check whether to activate auto-camera
		if (autoCamera)
			applyAutoCameraTransformation(gl);
		else
			applyViewTransformation(gl);
		
		// set light position and direction
		if (!autoLight) {
			gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, light0_position[currentLightIndex], 0);
			gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPOT_DIRECTION, light0_spotDirection[currentLightIndex], 0);
			lastLightPosition = light0_position[currentLightIndex].clone();
		} else {
			applyAutoLightTransformation(gl);
		}
		
		// draw billiard table
		if (drawSceneShadowed) {
			// paused to avoid shadowing of balls while they are moving due to
			// volume shadowing
			billiardTable.pause();
			scene_shadowed.draw(gl);
			billiardTable.resume();
		} else {
			scene.draw(gl);
		}
		
		// draw shadow volumes (if requested)
		if (drawShadowVolumes)
			scene_shadowed.draw_shadowVolume(gl);
		
		// draw glowing billiard table
		if (drawBilliardTable_glowified == true)
			billiardTable_glowified.draw(gl);
		
		// draw cue indicator
		if (drawCueIndicator == true) {
			billiardTable.getCueIndicator().draw(gl);
			if (drawCueIndicator_glowified == true)
				cueIndicator_glowified.draw(gl);
		}
		
		// draw ashtray
		model_ashtray.draw(gl);
		
		// draw smoke
		if (drawSmoke)
			smokeEngine.updateParticles(gl);
		
		// draw light source
		if (drawLightSource) {
			gl.glDisable(GL.GL_TEXTURE_2D);
			gl.glDisable(GL.GL_LIGHTING);
			gl.glColor4d(1., 1., 0., 1.);
			gl.glPushMatrix();
			gl.glTranslatef(lastLightPosition[0], lastLightPosition[1], lastLightPosition[2]);
			model_light.draw(gl);
			light_glowified.draw(gl);
			gl.glPopMatrix();
			gl.glEnable(GL.GL_LIGHTING);
		}
		
		// draw configuration menu
		if (showConfigurationMenu)
			cfgMenu.draw(drawable);
		
		// restore save OpenGL state variables
		gl.glPopMatrix();
		
//		// check whether we are in slow machine mode
//		if (slowMachine == true)
//			billiardTable.resume();
	}
	
	/**
	 * Initializes the OpenGL context.
	 */
	@Override
	public void init(GLAutoDrawable drawable) {
		super.init(drawable);
		GL gl = drawable.getGL();
		
		// register key listener
		drawable.addKeyListener(this);
		
		// configuration of OpenGL
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glCullFace(GL.GL_BACK);
		
		// configure and enable light0
		gl.glEnable(GL.GL_LIGHTING);
		gl.glEnable(GL.GL_LIGHT0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, light0_ambient, 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, light0_diffuse, 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, light0_specular, 0);
		gl.glLightf(GL.GL_LIGHT0, GL.GL_SPOT_EXPONENT, light0_spotExponent);
		gl.glLightf(GL.GL_LIGHT0, GL.GL_SPOT_CUTOFF, light0_spotCutoff);
		
		// configure OpenGL light model
		float[] lightmodel_ambient = { 0.5f, 0.5f, 0.5f, 1.0f };
		gl.glLightModelfv(GL.GL_LIGHT_MODEL_AMBIENT, lightmodel_ambient, 0);
		
		// initialize billiard table
		billiardTable.init(gl);
		
		// initialize smoke engine
		smokeEngine.init(gl);
		
		// load some models
		model_ashtray = new TranslatedOpenGLDrawable(WavefrontObjLoader.loadObject(gl, MODEL_ASHTRAY),
		        smokePosition[0], smokePosition[1], smokePosition[2]);
		model_light = WavefrontObjLoader.loadObject(gl, MODEL_LIGHTSOURCE);
		
		// create scene group
		scene = new OpenGLDrawableGroup();
		scene.add(billiardTable);
		scene.add(WavefrontObjLoader.loadObject(gl, MODEL_FLOOR));
		
		// setup shadower for scene
		scene_shadowed = new Shadower(scene);
		scene_shadowed.init(gl);
		
		// setup glowifiers
		billiardTable_glowified = new Glowifier(billiardTable, 3., 4.5, 0.3);
		billiardTable_glowified.init(gl, 100, 100);
		cueIndicator_glowified = new Glowifier(billiardTable.getCueIndicator(), 3., 4.5, 0.4);
		cueIndicator_glowified.init(gl, 100, 100);
		light_glowified = new Glowifier(model_light, 6., 6., 0.5);
		light_glowified.init(gl, 500, 500);
		
		// //Find and connect to a Wii Remote
		// try {
		// System.out.println("Connect WiiRemote");
		// WiiRemote remote = WiiRemoteJ.findRemote();
		// remote.addWiiRemoteListener(this);
		// remote.setAccelerometerEnabled(true);
		// remote.setSpeakerEnabled(true);
		// remote.setIRSensorEnabled(true, WRIREvent.BASIC);
		// remote.setLEDIlluminated(0, true);
		//        
		// remote.getButtonMaps().add(new ButtonMap(WRButtonEvent.HOME, ButtonMap.NUNCHUK, WRNunchukExtensionEvent.C,
		// new int[]{java.awt.event.KeyEvent.VK_CONTROL},
		// java.awt.event.InputEvent.BUTTON1_MASK, 0, -1));
		// } catch (InterruptedException e) {
		// } catch (IOException e) {
		// }
	}
	
	/**
	 * Keyboard handling.
	 */
	public void keyPressed(KeyEvent e) {
		// control next cue stroke
		if (e.getKeyCode() == KeyEvent.VK_A)
			billiardTable.increaseCueVelocity();
		if (e.getKeyCode() == KeyEvent.VK_Y)
			billiardTable.decreaseCueVelocity();
		if (e.getKeyCode() == KeyEvent.VK_UP)
			billiardTable.rotateCue_up();
		if (e.getKeyCode() == KeyEvent.VK_DOWN)
			billiardTable.rotateCue_down();
		if (e.getKeyCode() == KeyEvent.VK_LEFT)
			billiardTable.rotateCue_left();
		if (e.getKeyCode() == KeyEvent.VK_RIGHT)
			billiardTable.rotateCue_right();
		
		// stroke with the cue
		if (e.getKeyCode() == KeyEvent.VK_SPACE)
			billiardTable.cueStroke();
		
		// enable/disable automatic camera movement
		if (e.getKeyCode() == KeyEvent.VK_C) {
			autoCamera = !autoCamera;
			
			// backup/restore original camera position/orientation
			if (autoCamera == false) {
				cameraPos = cameraPos_backup;
				rotx_camera = rotx_camera_backup;
				roty_camera = roty_camera_backup;
				rotz_camera = rotz_camera_backup;
			} else {
				cameraPos_backup = cameraPos.clone();
				rotx_camera_backup = rotx_camera;
				roty_camera_backup = roty_camera;
				rotz_camera_backup = rotz_camera;
			}
		}
		
		// enable/disable automatic light movement
		if (e.getKeyCode() == KeyEvent.VK_T)
			autoLight = !autoLight;
		
		// reset ball positions
		if (e.getKeyCode() == KeyEvent.VK_R)
			billiardTable.reset();
		
		// reset cue ball
		if (e.getKeyCode() == KeyEvent.VK_B)
			billiardTable.resetCueBall();
		
		// enable/disable cueIndicator
		if (e.getKeyCode() == KeyEvent.VK_I)
			drawCueIndicator = !drawCueIndicator;
		
		// enable/disable shadowing
		if (e.getKeyCode() == KeyEvent.VK_S)
			drawSceneShadowed = !drawSceneShadowed;
		
		// enable/disable drawing of shadow volumes
		if (e.getKeyCode() == KeyEvent.VK_V)
			drawShadowVolumes = !drawShadowVolumes;
		
		// iterate over the different light positions
		if (e.getKeyCode() == KeyEvent.VK_L)
			currentLightIndex = (currentLightIndex + 1) % light0_position.length;
		
		// enable/disable drawing of light source
		if (e.getKeyCode() == KeyEvent.VK_O)
			drawLightSource = !drawLightSource;
		
		// enable/disable glow effects
		if (e.getKeyCode() == KeyEvent.VK_1)
			drawBilliardTable_glowified = !drawBilliardTable_glowified;
		if (e.getKeyCode() == KeyEvent.VK_2)
			drawCueIndicator_glowified = !drawCueIndicator_glowified;
		
		// enable/disable smoke engine
		if (e.getKeyCode() == KeyEvent.VK_N)
			drawSmoke = !drawSmoke;
		
		// enable/disable gravity for balls
		if (e.getKeyCode() == KeyEvent.VK_G)
			billiardTable.toggleGravity();
		
		// enable slow-machine mode (time will be paused for balls during
		// rendering)
		if (e.getKeyCode() == KeyEvent.VK_M)
			slowMachine = !slowMachine;
		
		// pauses/resumes the table
		if (e.getKeyCode() == KeyEvent.VK_P) {
			billiardTablePaused = !billiardTablePaused;
			if (billiardTablePaused)
				billiardTable.pause();
			else
				billiardTable.resume();
		}
		
		// toggle configuration menu
		if (e.getKeyCode() == KeyEvent.VK_H)
			showConfigurationMenu = !showConfigurationMenu;
	}
	
	public void keyReleased(KeyEvent e) { /* void */
	}
	
	public void keyTyped(KeyEvent e) { /* void */
	}
	
	// public void accelerationInputReceived(WRAccelerationEvent evt) {
	// System.out.println("Z : " + evt.getZAcceleration());
	// System.out.println("Roll: " + evt.getRoll());
	// if (evt.getZAcceleration() > 3.0)
	// billiardTable.cueStroke();
	// if (evt.getRoll() < -0.5)
	// billiardTable.rotateCue_left();
	// if (evt.getRoll() > 0.5)
	// billiardTable.rotateCue_right();
	// }
	//
	// public void buttonInputReceived(WRButtonEvent evt) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void combinedInputReceived(WRCombinedEvent evt) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void disconnected() {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void extensionConnected(WiiRemoteExtension extension) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void extensionDisconnected(WiiRemoteExtension extension) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void extensionInputReceived(WRExtensionEvent evt) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void extensionPartiallyInserted() {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void extensionUnknown() {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void IRInputReceived(WRIREvent evt) {
	// // TODO Auto-generated method stub
	//	    
	// }
	//
	// public void statusReported(WRStatusEvent evt) {
	// // TODO Auto-generated method stub
	//	    
	// }
}
